﻿//===============================================================================
// Microsoft patterns & practices Enterprise Library Contribution
// Logging Application Block
//===============================================================================

using System;
using System.Data;
using System.Data.Common;
using System.Diagnostics;
using System.Globalization;
using Microsoft.Practices.EnterpriseLibrary.Common.Configuration;
using Data = Microsoft.Practices.EnterpriseLibrary.Data;
using Microsoft.Practices.EnterpriseLibrary.Logging;
using Microsoft.Practices.EnterpriseLibrary.Logging.Formatters;
using Microsoft.Practices.EnterpriseLibrary.Logging.TraceListeners;
using EntLibContrib.Logging.Database.Configuration;

namespace EntLibContrib.Logging.Database
{
	/// <summary>
	/// A <see cref="System.Diagnostics.TraceListener"/> that writes to a database, formatting the output with an <see cref="ILogFormatter"/>.
	/// </summary>
	/// <remarks>The original assumed the database used would support stored procedures. This extended
	/// version works with sql strings as well as stored procedures to work with all supported db types.</remarks>
	[ConfigurationElementType(typeof(ExtendedDatabaseTraceListenerData))]
	public class ExtendedDatabaseTraceListener : FormattedTraceListenerBase
	{
		#region Fields
		string writeLogSqlCommand = String.Empty;
		string addCategorySqlCommand = String.Empty;
		CommandType commandType = CommandType.StoredProcedure;
		Data.Database database;
		#endregion

		#region Construction
		/// <summary>
		/// Initializes a new instance of <see cref="ExtendedDatabaseTraceListener"/>.
		/// </summary>
		/// <param name="database">The database for writing the log.</param>
		/// <param name="writeLogSqlCommand">The SQL command text or procedure to write to the log.</param>
		/// <param name="addCategorySqlCommand">The SQL command text or procedure to add a category.</param>
		/// <param name="commandType">Sql command type, either stored procedure or text.</param>
		/// <param name="formatter">The formatter.</param>
		public ExtendedDatabaseTraceListener(Data.Database database, string writeLogSqlCommand, string addCategorySqlCommand, CommandType commandType, ILogFormatter formatter)
			: base(formatter)
		{
			this.writeLogSqlCommand = writeLogSqlCommand;
			this.addCategorySqlCommand = addCategorySqlCommand;
			this.commandType = commandType;
			this.database = database;
		}
		#endregion

		#region Public Methods
		/// <summary>
		/// The Write method 
		/// </summary>
		/// <param name="message">The message to log</param>
		public override void Write(string message)
		{
			ExecuteWriteLogSqlCommand(0, 5, TraceEventType.Information, string.Empty, DateTime.Now, string.Empty, string.Empty, string.Empty, string.Empty, null, null, message, database);
		}

		/// <summary>
		/// The WriteLine method.
		/// </summary>
		/// <param name="message">The message to log</param>
		public override void WriteLine(string message)
		{
			Write(message);
		}

		/// <summary>
		/// Delivers the trace data to the underlying database.
		/// </summary>
		/// <param name="eventCache">The context information provided by <see cref="System.Diagnostics"/>.</param>
		/// <param name="source">The name of the trace source that delivered the trace data.</param>
		/// <param name="eventType">The type of event.</param>
		/// <param name="id">The id of the event.</param>
		/// <param name="data">The data to trace.</param>
		public override void TraceData(TraceEventCache eventCache, string source, TraceEventType eventType, int id, object data)
		{
			if ((this.Filter == null) || this.Filter.ShouldTrace(eventCache, source, eventType, id, null, null, data, null))
			{
				LogEntry logEntry = data as LogEntry;
				if (logEntry != null)
				{
					if (ValidateParameters(logEntry))
						ExecuteSqlCommands(logEntry);
					InstrumentationProvider.FireTraceListenerEntryWrittenEvent();
				}
				else
				{
					string stringEntry = data as string;
					if (stringEntry != null)
					{
						Write(stringEntry);
					}
					else
						base.TraceData(eventCache, source, eventType, id, data);
				}	
			}
		}
		#endregion

		#region Protected Methods
		/// <summary>
		/// Declare the supported attributes for <see cref="ExtendedDatabaseTraceListener"/>
		/// </summary>
		protected override string[] GetSupportedAttributes()
		{
			return new string[4] { "formatter", "writeLogSqlCommand", "addCategorySqlCommand", "commandType", "databaseInstanceName" };
		}
		#endregion

		#region Private Methods
		/// <summary>
		/// Validates that enough information exists to attempt executing the sql command
		/// </summary>
		/// <param name="logEntry">The LogEntry to validate.</param>
		/// <returns>A boolean indicating whether the parameters for the LogEntry configuration are valid.</returns>
		private bool ValidateParameters(LogEntry logEntry)
		{
			bool valid = true;

			if (string.IsNullOrEmpty(writeLogSqlCommand))
			{
				return false;
			}

			if (string.IsNullOrEmpty(addCategorySqlCommand))
			{
				return false;
			}

			if (commandType == CommandType.TableDirect)
			{
				return false;
			}

			return valid;
		}

		/// <summary>
		/// Executes the sql commands
		/// </summary>
		/// <param name="logEntry">The LogEntry to store in the database</param>
		private void ExecuteSqlCommands(LogEntry logEntry)
		{
			using (DbConnection connection = database.CreateConnection())
			{
				connection.Open();
				try
				{
					using (DbTransaction transaction = connection.BeginTransaction())
					{
						try
						{
							int logID = Convert.ToInt32(ExecuteWriteLogSqlCommand(logEntry, database, transaction));
							ExecuteAddCategorySqlCommand(logEntry, logID, database, transaction);
							transaction.Commit();
						}
						catch
						{
							transaction.Rollback();
							throw;
						}
					}
				}
				finally
				{
					connection.Close();
				}
			}
		}

		/// <summary>
		/// Executes the WriteLog sql command
		/// </summary>
		/// <param name="eventId">The event id for this LogEntry.</param>
		/// <param name="priority">The priority for this LogEntry.</param>
		/// <param name="severity">The severity for this LogEntry.</param>
		/// <param name="title">The title for this LogEntry.</param>
		/// <param name="timeStamp">The timestamp for this LogEntry.</param>
		/// <param name="machineName">The machine name for this LogEntry.</param>
		/// <param name="appDomainName">The appDomainName for this LogEntry.</param>
		/// <param name="processId">The process id for this LogEntry.</param>
		/// <param name="processName">The processName for this LogEntry.</param>
		/// <param name="managedThreadName">The managedthreadName for this LogEntry.</param>
		/// <param name="win32ThreadId">The win32threadID for this LogEntry.</param>
		/// <param name="message">The message for this LogEntry.</param>
		/// <param name="db">An instance of the database class to use for storing the LogEntry</param>
		/// <returns>An integer for the LogEntry Id</returns>
		private int ExecuteWriteLogSqlCommand(int eventId, int priority, TraceEventType severity, string title, DateTime timeStamp,
																					string machineName, string appDomainName, string processId, string processName,
																					string managedThreadName, string win32ThreadId, string message, Data.Database database)
		{
			DbCommand command = BuildCommand(database, writeLogSqlCommand);
			BuildParameters(database, command);

			database.SetParameterValue(command, "eventID", eventId);
			database.SetParameterValue(command, "priority", priority);
			database.SetParameterValue(command, "severity", severity.ToString());
			database.SetParameterValue(command, "title", title);
			database.SetParameterValue(command, "timestamp", timeStamp);
			database.SetParameterValue(command, "machineName", machineName);
			database.SetParameterValue(command, "AppDomainName", appDomainName);
			database.SetParameterValue(command, "ProcessID", processId);
			database.SetParameterValue(command, "ProcessName", processName);
			database.SetParameterValue(command, "ThreadName", managedThreadName);
			database.SetParameterValue(command, "Win32ThreadId", win32ThreadId);
			database.SetParameterValue(command, "message", message);
			database.SetParameterValue(command, "formattedmessage", message);

			database.ExecuteNonQuery(command);
			int logId = Convert.ToInt32(command.Parameters[command.Parameters.Count - 1].Value, CultureInfo.InvariantCulture);
			return logId;
		}

		/// <summary>
		/// Executes the WriteLog sql command
		/// </summary>
		/// <param name="logEntry">The LogEntry to store in the database.</param>
		/// <param name="db">An instance of the database class to use for storing the LogEntry</param>
		/// <param name="transaction">The transaction that wraps around the execution calls for storing the LogEntry</param>
		/// <returns>An integer for the LogEntry Id</returns>
		private int ExecuteWriteLogSqlCommand(LogEntry logEntry, Data.Database db, DbTransaction transaction)
		{
			DbCommand command = BuildCommand(database, writeLogSqlCommand);
			BuildParameters(database, command);

			database.SetParameterValue(command, "eventID", logEntry.EventId);
			database.SetParameterValue(command, "priority", logEntry.Priority);
			database.SetParameterValue(command, "severity", logEntry.Severity.ToString());
			database.SetParameterValue(command, "title", logEntry.Title);
			database.SetParameterValue(command, "timestamp", logEntry.TimeStamp);
			database.SetParameterValue(command, "machineName", logEntry.MachineName);
			database.SetParameterValue(command, "AppDomainName", logEntry.AppDomainName);
			database.SetParameterValue(command, "ProcessID", logEntry.ProcessId);
			database.SetParameterValue(command, "ProcessName", logEntry.ProcessName);
			database.SetParameterValue(command, "ThreadName", logEntry.ManagedThreadName);
			database.SetParameterValue(command, "Win32ThreadId", logEntry.Win32ThreadId);
			database.SetParameterValue(command, "message", logEntry.Message);
			if (Formatter != null)
				database.SetParameterValue(command, "formattedmessage", Formatter.Format(logEntry));
			else
				database.SetParameterValue(command, "formattedmessage", logEntry.Message);

			db.ExecuteNonQuery(command, transaction);
			int logId = Convert.ToInt32(command.Parameters[command.Parameters.Count - 1].Value, CultureInfo.InvariantCulture);
			return logId;
		}

		/// <summary>
		/// Executes the AddCategory sql command
		/// </summary>
		/// <param name="logEntry">The LogEntry to store in the database.</param>
		/// <param name="logID">The unique identifer for the LogEntry as obtained from the WriteLog sql command.</param>
		/// <param name="db">An instance of the database class to use for storing the LogEntry</param>
		/// <param name="transaction">The transaction that wraps around the execution calls for storing the LogEntry</param>
		private void ExecuteAddCategorySqlCommand(LogEntry logEntry, int logID, Data.Database db, DbTransaction transaction)
		{
			DbCommand cmd = BuildCommand(db, addCategorySqlCommand);
			db.AddInParameter(cmd, "categoryName", DbType.String);
			db.AddInParameter(cmd, "logID", DbType.Int32);

			foreach (string category in logEntry.Categories)
			{
				db.SetParameterValue(cmd, "categoryName", category);
				db.SetParameterValue(cmd, "logID", logID);
				db.ExecuteNonQuery(cmd, transaction);
			}
		}

		/// <summary>
		/// Builds the database command.
		/// </summary>
		/// <param name="database">The database instance.</param>
		/// <param name="sqlCommand">The sql command to run.</param>
		/// <returns>
		/// instantiated <see cref="DbCommand"/> object
		/// </returns>
		private DbCommand BuildCommand(Data.Database database, string sqlCommand)
		{
			DbCommand dbCommand = null;
			if (commandType == CommandType.StoredProcedure)
				dbCommand = database.GetStoredProcCommand(sqlCommand);
			else
				dbCommand = database.GetSqlStringCommand(sqlCommand);
			return dbCommand;
		}

		/// <summary>
		/// Builds the command parameters.
		/// </summary>
		/// <param name="database">The database instance.</param>
		/// <param name="sqlCommand">The sql command to run.</param>
		private static void BuildParameters(Data.Database database, DbCommand sqlCommand)
		{
			database.AddInParameter(sqlCommand, "eventID", DbType.Int32);
			database.AddInParameter(sqlCommand, "priority", DbType.Int32);
			database.AddInParameter(sqlCommand, "severity", DbType.String);
			database.AddInParameter(sqlCommand, "title", DbType.String);
			database.AddInParameter(sqlCommand, "timestamp", DbType.DateTime);
			database.AddInParameter(sqlCommand, "machineName", DbType.String);
			database.AddInParameter(sqlCommand, "AppDomainName", DbType.String);
			database.AddInParameter(sqlCommand, "ProcessID", DbType.String);
			database.AddInParameter(sqlCommand, "ProcessName", DbType.String);
			database.AddInParameter(sqlCommand, "ThreadName", DbType.String);
			database.AddInParameter(sqlCommand, "Win32ThreadId", DbType.String);
			database.AddInParameter(sqlCommand, "message", DbType.String);
			database.AddInParameter(sqlCommand, "formattedmessage", DbType.String);
			database.AddOutParameter(sqlCommand, "LogId", DbType.Int32, 4);
		}
		#endregion
	}
}
